﻿/*  ----------------------------------------------------------------------------
 *  Copyright somemorebytes 2010
	DavidP.
	somemorebytes@gmail.com
 *  ----------------------------------------------------------------------------
 *  Simple EXIF Renamer
 *  ----------------------------------------------------------------------------
 *  File:       Renamer.cs
 *  ----------------------------------------------------------------------------


This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

*/


using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Threading;
using System.Drawing;
using System.Drawing.Imaging;

namespace SimpleExifRenamer
{

    //Delegate to change the status of the progressbar on the main threar from the process thread
    public  delegate void RefreshProgressBarDelegate(decimal valor);

    //Delegate to change the GUI when the process thread ends.
    public delegate  void FinishProcessDelegate(bool clean);

    //Delegate to show a message and treat an exception in the main thread from the process thread
    public delegate void TreatException(Exception w);


    class Renamer
    {

        //Create year subfolder?
       public  bool yearSubDir = false;

        //Create month subfolder
        public bool monthSubDir = false;

        //Check every filename for invalid chars
        public bool checkFilename = false;

        //pattern to rename
        public string renamePattern;

        public bool movefiles = false;

        //Root folder
        public string rootTargetDir;

        //list of images added to process
        public List<string> processQueue;

        //List of pictures with errors after the process
        public List<string> errorQueue;

        //List of pictures without EXIF date info embedded. Will be copied to the NO_EXIF_INFO folder
        public List<string> noDateQueue;

        //List of files added, which are not pictures. Probably videos or other stuff. Will be copied to the NO_JPG_FILE folder
        public List<string> noPictures;

        //List of files that are not pictures, nor multimedia files. Simple Exif Renamer will not touch them.
        public List<string> notProcessed;

        //A reference to the GUI. IT is necessary, so we can update GUI elements from some methods here that will be in another thread.
        public MainForm gui;

        //Reference to the thread started (to be able to kill it)
        Thread processThread=null;

        //Check if the thread is running
        public bool ProcessRunning { 
            get 
            {
                if (this.processThread == null)
                {
                    return false;
                }
                else
                {
                    return this.processThread.IsAlive;
                }
                
            }
        }

        //Folder name that will be created with those images impossible to process
        public string folderNameNotDate = "NO_EXIF_INFO";

        //Folder name that will be created with those files that are not pictures
        public string folderNameNotPicture = "NO_JPG_FILE";

        
        public Renamer()
        {
            this.processQueue = new List<string>();
            this.errorQueue = new List<string>();
            this.noDateQueue = new List<string>();
            this.noPictures = new List<string>();
            this.notProcessed = new List<string>();
        }

        
        //Given a Folder, recursively extracts all the files inside and tries to add them to the procesQueue
        public void addFolderRecursive(string path)
        {
           //List containing all the files
            List<string> result = new List<string>();

            //Stack of fodlers to explore
            Stack<string> stack = new Stack<string>();

            //init
            stack.Push(path);

            //Explore all folders
            while (stack.Count > 0)
            {
                string dir = stack.Pop();

                try
                {
                    
                    //this.processQueue.AddRange(Directory.GetFiles(dir, "*.jpg"));
                    string[] files = Directory.GetFiles(dir);

                    foreach (string filepth in files)
                    {
                        this.addFile(filepth);

                    }

                    //Push subfolders on the stack
                    foreach (string dn in Directory.GetDirectories(dir))
                    {
                        stack.Push(dn);
                    }
                }
                catch
                {
                    
                    throw;
                }
            }
            
            
        }

        //Adds a file. If it is an image with a permited extension, it will add the image to the processQueue
        public void addFile(string path)
        {
            string ext = Path.GetExtension(path).ToLower();
            if ((ext == ".jpg"))
            {
                this.processQueue.Add(path); 
            }
            else //If the file is not a picture, add it to the list NoPictures (only for supporter types)
            {
                if ((ext == ".mov") || (ext == ".avi") || (ext == ".wmv") || (ext == ".mpg") || (ext == ".mpeg") || (ext == ".wav") || (ext == ".mp3") || (ext == ".mp4") || (ext == ".3gp") || (ext == ".psd"))
                {
                    this.noPictures.Add(path);
                }
                else
                {
                    this.notProcessed.Add(path);
                }
            }

            

        }


        //Starts the thread with the renaming process
        public void startProcessing()
        {
            try
            {
                if (!this.ProcessRunning)
                {
                  
                    //Clear the error queues
                    this.errorQueue.Clear();
                    this.noDateQueue.Clear();
                  

                    this.processThread = new Thread(this.Process);
                    this.processThread.Start();
                }
            }
            catch //HEre , only exceptions when STARTING the thread would be catched. Not the ones on the thread itself.
            {
                throw;
            }
        }

        //Kill the processing thread
        public void stopProcessing()
        {
            if (this.ProcessRunning)
            {
                this.processThread.Abort();
                this.processThread = null;
            }
        }

        //Start processing the queue
        public void Process()
        {

            decimal completed;
            int elements=this.processQueue.Count;
            
            //Convert the pattern to a string pattern
            this.renamePattern = this.renamePattern.Replace("{Year}", "{0}").Replace("{Month}", "{1}").Replace("{Day}", "{2}").Replace("{Hour}", "{3}").Replace("{Min}", "{4}").Replace("{Sec}", "{5}");

            try
            {//Do the renaming process
                for (int i = 0; i < elements; i++)
                {
                    //Process the picture
                    
                    this.renameIt(this.processQueue[i]);

                    //Update progress bar
                    completed = ((decimal)(i + 1) / (decimal)elements) * 100;
                    this.gui.ProgressBarValue = completed;

                }

                //Now we'll need to process the non-picture files to copy them to the appropiate folder
                if (this.noPictures.Count > 0)
                {
                    string nopicturepath = Path.Combine(this.rootTargetDir, this.folderNameNotPicture);
                    if (!System.IO.Directory.Exists(nopicturepath)) System.IO.Directory.CreateDirectory(nopicturepath);

                    foreach (string filepath in this.noPictures)
                    {
                        string fullpath = Path.Combine(nopicturepath, Path.GetFileName(filepath));

                        if (File.Exists(fullpath)) fullpath = this.findAlternateName(fullpath);

                        if (!this.movefiles)
                        {
                            File.Copy(filepath, fullpath);
                        }
                        else
                        {
                            File.Move(filepath, fullpath);
                        }
                    }
                   
                }

                //process successfully finished!!!!

                this.createLog();
                this.gui.processFinishedUpdateGUI(true);

            }
            catch (Exception w) //Here the process really had a failure. Here we catch any unmanaged exception in the process thread. The exception must be managed on the main thread.
            {
                this.gui.treatException(w);
                return;
            }

        }

             
        //Rename a picture
        public void renameIt(string path)
        {
            try
            {
                //First we need to extract the date
                string datestr = this.extractEXIFDate(path);

                switch (datestr)
                {
                    case "Error": //Nothing to do here
                        return;

                    case "": //The file has no date
                        string noexifpath = Path.Combine(this.rootTargetDir, this.folderNameNotDate);
                        if (!System.IO.Directory.Exists(noexifpath)) System.IO.Directory.CreateDirectory(noexifpath);

                        string fullnoexiffilepath = Path.Combine(noexifpath, Path.GetFileName(path));

                        if (File.Exists(fullnoexiffilepath)) fullnoexiffilepath = this.findAlternateName(fullnoexiffilepath);

                        if (!this.movefiles)
                        {
                            File.Copy(path, fullnoexiffilepath);
                        }
                        else
                        {
                            File.Move(path, fullnoexiffilepath);
                        }

                        return;

                    default: //Everything ok. 
                        //Split the date
                        string[] parts = datestr.Split(':', ' ', '/');

                        parts = this.check_splitted_date(parts);

                        string picname = String.Format(this.renamePattern, parts);

                        //If the failsafe rename is activated, check the filename for invalid chars
                        if (this.checkFilename)
                        {
                            picname = this.checkFilenameChars(picname);
                        }
                                                
                        //Add the picture extension
                        picname += Path.GetExtension(path).ToLower();

                        //Create and check the path
                        string fullpath = this.rootTargetDir;

                        if (this.yearSubDir)
                        {
                            fullpath = Path.Combine(fullpath, parts[0]);
                        }

                        if (this.monthSubDir)
                        {
                            fullpath = Path.Combine(fullpath, parts[1]);
                        }

                        //Create the dir if it does not exists
                        if (!System.IO.Directory.Exists(fullpath)) System.IO.Directory.CreateDirectory(fullpath);

                        //We got the full path
                        fullpath = Path.Combine(fullpath, picname);

                        if (File.Exists(fullpath)) fullpath = this.findAlternateName(fullpath);

                        //Copy/Move the file!
                        if (!this.movefiles)
                        {
                            File.Copy(path, fullpath);
                        }
                        else
                        {
                            File.Move(path, fullpath);
                        }
                        break;

                }

            }
            catch (IOException)
            {
                //throw new Exception("In the file: " + path + "\n\n" + w.Message);
                //Probably the file was in use. Just leave it alone, and add it to the error list. No need to stop the program
                this.errorQueue.Add(path);
                return;
            }
            catch
            {
                throw;
            }

        }

        //Just for rare cases, check that the splitted date is correct
        public string[] check_splitted_date(string[] parts)
        {
            //Dirty Patch 1:
            //Some pictures may not have all the fields, maybe because they're corrupted, maybe because the camera was crazy!
            //This is absolutely inefficient, but it will only happen in really rare cases!!!
            int arr_last_element = parts.GetUpperBound(0);
            if (arr_last_element < 5)
            {
                //Reallocate the array
                string[] parts_aux = new string[6];
                System.Array.Copy(parts, parts_aux, arr_last_element + 1);
    
                //fill the array
                for (int i = arr_last_element + 1; i <= 5; i++)
                {
                    parts_aux[i] = "01";
                }

                //Failsafe filter
                for (int j = 0; j <= arr_last_element; j++)
                {
                    parts_aux[j] = this.checkFilenameChars(parts_aux[j]);
                }
              
                return parts_aux;
            }
            else
            {
                return parts;
            }

            
        }

        //Check that there are not invalid chars in the filename 
        public string checkFilenameChars(string filename)
        {
            //Just in case the picture was corrupted, and we have incorrect ascii chars when extracting the EXIF Date
            //Filter the string to be sure no invalid chars are on the filename
            foreach (char c in System.IO.Path.GetInvalidFileNameChars())
            {

                int pos = filename.IndexOf(c);
                while (pos != -1)
                {
                    filename = filename.Remove(pos, 1);
                    pos = filename.IndexOf(c);
                }
            }

            return filename;

        }

        //Generate the log file for this conversion
        public void createLog()
        {
            StreamWriter sw = File.CreateText(Path.Combine(this.rootTargetDir, "rename_log.txt"));
            sw.WriteLine("Total Files processed: " + this.processQueue.Count.ToString());
            sw.WriteLine("");
            sw.WriteLine("");
            sw.WriteLine("Files whitout EXIF info: " + this.noDateQueue.Count.ToString());
            sw.WriteLine("");
            sw.WriteLine("These files can be found in: " + Path.Combine(this.rootTargetDir, this.folderNameNotDate).ToString());
            sw.WriteLine("");
            foreach (string s in this.noDateQueue)
            {
                sw.WriteLine(s);
            }
            sw.WriteLine("");
            sw.WriteLine("");
            sw.WriteLine("Files that were not jpg pictures, but multimedia files: " + this.noPictures.Count.ToString());
            sw.WriteLine("");
            sw.WriteLine("These files can be found in: " + Path.Combine(this.rootTargetDir, this.folderNameNotPicture).ToString());
            sw.WriteLine("");
            foreach (string s in this.noPictures)
            {
                sw.WriteLine(s);
            }
            sw.WriteLine("");
            sw.WriteLine("");
            sw.WriteLine("Files that were not jpg pictures nor multimedia files: " + this.notProcessed.Count.ToString());
            sw.WriteLine("");
            sw.WriteLine("This files haven't been copied or moved anywhere, so you can find them in their original locations.");
            sw.WriteLine("");
            foreach (string s in this.notProcessed)
            {
                sw.WriteLine(s);
            }
            sw.WriteLine("");
            sw.WriteLine("");
            sw.WriteLine("Files that produced errors: " + this.errorQueue.Count.ToString());
            sw.WriteLine("");
            sw.WriteLine("This files haven't been copied or moved anywhere, so you can find them in their original locations.");
            sw.WriteLine("");
            foreach (string s in this.errorQueue)
            {
                sw.WriteLine(s);
            }

            sw.WriteLine("");
            sw.WriteLine("");
            sw.WriteLine("That's all folks.");
            sw.Close();

                    
        }

        //In case the name is already in use, we need to find a new one. This function will generate names in the form filename(X).extension until 
        //a nonexistent filename is found.
        public string findAlternateName(string path)
        {
            string pattern = Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path) + "({0})" + Path.GetExtension(path) );

            
            

            string aux="";
            bool found = true;
            int index=1;

            while (found)
            {
                aux = String.Format(pattern, index.ToString());

                if (!File.Exists(aux)) found = false;
                                   
                index++;
            }

            return aux;
            

        }

        //Extract the EXIF date from a picture. Returns "" if no date is found, and Error if there was an error retrieving the name
        public string extractEXIFDate(string path)
        {
            const int IDExifDate = 36868; //dec for DateTimeDigitized is 36868//For DateTimeOriginal set to 36867

            try
            {
                //Open the picture
                using (Bitmap mypict = new Bitmap(path))
                {

                    //Extract the properties
                    PropertyItem[] props = mypict.PropertyItems;
                    PropertyItem dateprop;
                    try
                    {
                        //Extract the exif date
                        dateprop = mypict.GetPropertyItem(IDExifDate);

                        Encoding ascii = Encoding.ASCII;
                        string datedigstr = ascii.GetString(dateprop.Value, 0, dateprop.Len - 1);

                        //Return the exif date
                        return datedigstr;


                    }
                    catch //The property does not exist
                    {
                        this.noDateQueue.Add(path);
                        return "";
                    }
                }
            }
            catch //The file is not good!
            {
                this.errorQueue.Add(path);
                return "Error";
            }
           
        }

        //Default renaming patterns
        public List<string> getPatterns()
        {

            List<string> patt = new List<string>();

            patt.Add("{Year}-{Month}-{Day}-({Hour}{Min}{Sec})");
            patt.Add("{Month}-{Day}-({Hour}{Min}{Sec})");
            patt.Add("{Day}-({Hour}{Min}{Sec})");
            patt.Add("{Day}-{Hour}-{Min}-{Sec}");
            patt.Add("{Day}-{Hour}{Min}{Sec}");


            return patt;
            
        }

        //Checks if a pattern is valid or not.
        ///No special chars
        public bool testPattern(string patt)
        {
            if (patt.IndexOfAny(System.IO.Path.GetInvalidFileNameChars()) != -1)
            {
                return false;
            }
            return true;
        }

        //Reset all the queues
        public void clearQueues()
        {
            this.processQueue.Clear();
            this.errorQueue.Clear();
            this.noDateQueue.Clear();
            this.noPictures.Clear();
            this.notProcessed.Clear();
        }

    }
}